// ==UserScript==
// @name         Base Package
// @namespace    https://gitee.com/yxphope/JSExtension
// @version      1.1.2
// @description  try to take over the world!
// @author       YXP
// @match        *://*/*
// @icon         
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @run-at       document-start
// @require      https://gitee.com/yxphope/JSExtension/raw/master/basePackage.js
// ==/UserScript==
/**
 * @createdDate  2024-05-22
 * @author       YXP
 * @description  扩展插件基础包
 */

(function () {
  "use strict";
  console.log("Base Package is loading...");
  // TamperMonkey提供的unsafeWindow不能被变量引用，无效；下面一行实现即使不在TamperMonkey环境中也能正常执行
  typeof unsafeWindow === "undefined" && (window.unsafeWindow = window);
  // 在启用多个require源时，防止重复执行
  if (unsafeWindow.$X) {
    return;
  }
  unsafeWindow.console.clear = ()=>{};
  // Blob钩子 遇到任何加密，先不要慌，直接上钩子拿明文
  var createBlob = unsafeWindow.URL.createObjectURL;
  unsafeWindow.URL.URL = {};
  unsafeWindow.URL.decryptObjectURL = (url)=>{
    return unsafeWindow.URL.URL[url];
  }
  // if (location.host === "www.douyin.com") {
  //   unsafeWindow.Blol = null;
  //   unsafeWindow.URL.createObjectURL = (s) => {
  //     return null;
  //   };
  // } else {
    unsafeWindow.URL.createObjectURL = (s) => {
      var r = createBlob(s);
      unsafeWindow.URL.URL[r] = s;
      return r;
    };
  // }
  /**
   * 选择器 返回第一个元素
   * @param {String} selector
   * @param {Element} context
   * @returns Element
   */
  const $X = (selector, context) => {
    return context ? context.querySelector(selector) : document.querySelector(selector);
  };
  $X.version = "1.1.2";
  unsafeWindow.$X = $X;
  /**
   * 选择器 返回所有匹配元素
   * @param {String} selector
   * @param {Element} context
   * @returns NodeList
   */
  $X.all = (selector, context) => {
    return context ? context.querySelectorAll(selector) : document.querySelectorAll(selector);
  };
  /**
   * Ajax请求（参数回调函数型）
   * @param {String} url URL地址
   * @param {Object} [options] 参数
   * @param {String} [options.method="GET"]  GET|POST|HEAD
   * @param {String} [options.type=GET]      GET|POST|HEAD
   * @param {Object} [options.header]        {key: value}
   * @param {Number} [options.timeout]       Number in ms
   * @param {Any}    [options.data]          String | Object
   * @param {Function} [options.success]       Function(xhr)
   * @param {Function} [options.error]         Function(xhr)
   */
  $X.ajax = (url, options) => {
    let xhr = new XMLHttpRequest();
    let method = (options.method || options.type || "GET").toUpperCase();
    if (method === "GET" && options.data) {
      let t = typeof options.data;
      t === "string"
        ? (url += "?" + options.data)
        : t === "object"
        ? ((url += "?"),
          Object.keys(options.data).forEach((k) => {
            url += k + "=" + encodeURIComponent(options.data[k]) + "&";
          }, url.endsWith("&") && (url = url.slice(0, -1))))
        : console.log("invalid data", method, url, data);
    }
    xhr.open(method, url);
    if (options.header instanceof Object) {
      for (const k in options.header) {
        xhr.setRequestHeader(k, options.header[k]);
      }
    }
    options.timeout && (xhr.timeout = options.timeout);
    xhr.onload = () => {
      xhr.status === 200 ? options.success instanceof Function && options.success(xhr) : options.error instanceof Function && options.error(xhr);
    };
    options.error instanceof Object &&
      (xhr.onerror = xhr.onabort =
        () => {
          options.error(xhr);
        });
    typeof options.data === "object" && ((options.data = JSON.stringify(options.data)), xhr.setRequestHeader("Content-Type", "application/json"));
    method === "GET" ? xhr.send() : options.data && xhr.send(options.data);
  };
  /**
   * Ajax请求 返回Promise，可await、then、catch同步调用
   * @param {String} type GET, POST
   * @param {String} url
   * @param {any}    [data]
   * @param {Object} [header]
   * @param {Number} [timeout=2000] 2000ms
   * @returns {Promise} resolve(xhr.response) reject(String|status)
   */
  $X.ajaxPro = async (type, url, data, header, timeout = 4000) => {
    return new Promise((res, rej) => {
      let xhr = new XMLHttpRequest();
      let method = type.toUpperCase();
      if (method === "GET" && data && !header) {
        (header = data), (data = undefined);
      }
      if (method === "GET" && data) {
        let t = typeof data;
        t === "string"
          ? (url += "?" + data)
          : t === "object"
          ? ((url += "?"),
            Object.keys(data).forEach((k) => {
              url += k + "=" + encodeURIComponent(data[k]) + "&";
            }, url.endsWith("&") && (url = url.slice(0, -1))))
          : console.log("invalid data", method, url, data);
      }
      xhr.open(method, url);
      if (header instanceof Object) {
        for (const k in header) {
          xhr.setRequestHeader(k, header[k]);
        }
      }
      timeout && (xhr.timeout = timeout);
      xhr.onload = () => {
        xhr.status === 200 ? res(xhr.response) : rej(xhr.response || xhr.status);
      };
      xhr.onerror = () => {
        rej("Request error");
      };
      xhr.ontimeout = () => {
        rej(`Timeout (${timeout})`);
      };
      xhr.onabort = () => {
        rej("Aborted");
      };
      typeof data === "object" && ((data = JSON.stringify(data)), xhr.setRequestHeader("Content-Type", "application/json"));
      method === "GET" ? xhr.send() : data && xhr.send(data);
    });
  };
  /**
   * document.documentElement
   */
  $X.docu = document.documentElement;
  /**
   * 创建元素
   * @param {String} tag
   * @param {String} [cla]
   * @param {Element} [father]
   * @param {String} [html]
   * @returns {Element}
   */
  $X.createEle = (tag, cla, father, html) => {
    let e = document.createElement(tag);
    cla && (e.className = cla);
    html && (e.innerHTML = html);
    father && father.append(e);
    return e;
  };
  /**
   * 添加CSS样式
   * @param {String} style
   */
  $X.addStyle = (style) => {
    $X.createEle("style", "custom_style", document.head, style);
  };
  $X.aTag = $X.createEle("a", 0, $X.docu);
  /**
   * 保存数据为文件并下载
   * @param {Any} data
   * @param {String} name
   * @param {String} [type="text/plain"] text/plain  image/jpeg  audio/mp3  video/mpeg
   */
  $X.saveFile = (data, name, type = "text/plain") => {
    const blob = new Blob([data], {
      type: type,
    });
    const objectURL = URL.createObjectURL(blob);
    $X.aTag.href = objectURL;
    $X.aTag.download = name;
    $X.aTag.click();
    URL.revokeObjectURL(objectURL);
  };
  /**
   * 自动翻页
   * @param {Number} [time=2000] time per page in ms (2000ms by default)
   * @param {Element} [ele] document.documentElement by default
   * @returns {number} Interval number
   */
  $X.autoPage = (time = 2000, ele) => {
    ele = ele || $X.docu;
    let y = window.scrollY,
      H = ele.offsetHeight,
      dy = ele.clientHeight;
    $X.autoPage.itv = setInterval(() => {
      ele.scrollTo(0, (y += dy));
      y >= H && clearInterval($X.autoPage.itv);
    }, time);
    return $X.autoPage.itv;
  };
  $X.autoPage.stop = ()=>{clearInterval($X.autoPage.itv);}
  /**
   * 选中元素区间
   * @param {Element} startNode 起始元素，若只有startNode一个参数，则选中本元素
   * @param {Element} endNode 结束元素
   */
  $X.setSelection = (startNode, endNode) => {
    var selection = window.getSelection();
    selection.removeAllRanges();
    var range = document.createRange();
    if (endNode) {
      range.setStartBefore(startNode);
      range.setEndAfter(endNode);
    } else if(startNode) {
      range.selectNode(startNode);
    } else{return}
    selection.addRange(range);
  };
  /**
   * 格式化字符串
   * @param {String} format  -04.3difs
   * @param  {...any} args
   * @returns String
   */
  $X.sprintf = (format, ...args) => {
    let i = 0;
    return format.replace(/%([-+0 ]?)(\d+)?(\.\d+)?([difs])/g, (match, flag, width, precision, type) => {
      let arg = args[i++];
      let result;
      // Handle different types
      switch (type) {
        case "s":
          result = String(arg);
          precision && (result = result.slice(0, parseInt(precision.slice(1), 10)));
          break;
        case "d":
        case "i":
          result = parseInt(arg, 10).toString();
          break;
        case "f":
          precision = precision ? parseInt(precision.slice(1), 10) : 6;
          result = parseFloat(arg).toFixed(precision);
          break;
        default:
          result = match;
          i--;
      }
      // Handle width and flags
      width = width ? parseInt(width, 10) : 0;
      if (width > result.length) {
        let padChar = " ";
        flag.includes("0") && !flag.includes("-") && (padChar = "0");
        result = flag.includes("-") ? result.padEnd(width, padChar) : result.padStart(width, padChar);
      }
      return result;
    });
  };
  /**
   * 格式化时间
   * @param {String} format %Y4位 / %y实际位数，%M/%m，%D/%d，%H/%h%M
   * @param {Date} [time]
   */
  $X.strfTime = (format, time) => {
    if (time instanceof Number) {
      time = new Date(time);
    } else if (!(time instanceof Date)) {
      time = new Date();
    }
    return format.replace(/%([YymdHMSs%])/g, (match, flag) => {
      let s = "";
      switch (flag) {
        case "Y":
          s = $X.sprintf("%04d", time.getFullYear());
          break;
        case "y":
          s = $X.sprintf("%02d", time.getFullYear() % 100);
          break;
        case "m":
          s = $X.sprintf("%02d", time.getMonth());
          break;
        case "d":
          s = $X.sprintf("%02d", time.getDate());
          break;
        case "H":
          s = $X.sprintf("%02d", time.getHours());
          break;
        case "M":
          s = $X.sprintf("%02d", time.getMinutes());
          break;
        case "S":
          s = $X.sprintf("%02d", time.getSeconds());
          break;
        case "s":
          s = $X.sprintf("%03d", time.getMilliseconds());
          break;
        case "%":
          s = "%";
          break;
        default:
          s = match;
      }
      return s;
    });
  };
  $X.copyOnSelect = () => {
    $X.copyOnSelect.status = !$X.copyOnSelect.status;
    $X.copyOnSelect.selecting = false;
    $X.copyOnSelect.itv = 0;
    $X.copyOnSelect.last = "";
    $X.copyOnSelect.fun = () => {
      $X.copyOnSelect.selecting ||
        ($X.copyOnSelect.itv = setInterval(() => {
          if (!$X.copyOnSelect.selecting) {
            let s = document.getSelection().toString();
            if (s && s !== $X.copyOnSelect.last) {
              navigator.clipboard.writeText(s);
              $X.copyOnSelect.last = s;
            }
            clearInterval($X.copyOnSelect.itv);
          }
          $X.copyOnSelect.selecting = false;
        }, 1500));
      $X.copyOnSelect.selecting = true;
    };
    $X.copyOnSelect.onCopy = () => {
      $X.copyOnSelect.selecting = false;
      clearInterval($X.copyOnSelect.itv);
    };
    $X.copyOnSelect.status ? (document.addEventListener("selectionchange", $X.copyOnSelect.fun), document.addEventListener("copy", $X.copyOnSelect.onCopy), document.addEventListener("cut", $X.copyOnSelect.onCopy)) : document.removeEventListener("selectionchange", $X.copyOnSelect.fun);
  };
  $X.copy = (s)=>{
    navigator.clipboard.writeText(s);
  }
  $X.on = (keys, callback)=>{
    // ctrl+shift+alt+s
  }
  $X.init = () => {
    $X.copyOnSelect();
    document.addEventListener('keydown',()=>{});
  };
  $X.init();

  /**
   * 向本地服务器发送cmd命令
   * @param {String} cmd
   * @param {Function} [callback=()=>{}] function(xhr.responseText)
   * @param {Boolean} [ret=false] false
   * @param {Number} [timeout=3] in seconds
   */
  $X.runcmd = (cmd, callback = () => {}, ret = false, timeout = 3) => {
    $X.ajax("http://localhost", {
      method: "POST",
      header: { token: "_^WUSV" },
      data: { oprt: "cmd", cmd: cmd, return: ret, timeout: timeout },
      success: (x) => {
        callback(x.responseText);
      },
    });
  };
  /**
   * 从本地服务器中获取文件
   * @param {String} filename
   * @param {Function} callback function(xhr)
   */
  $X.getLocal = (filename, callback) => {
    if (filename.indexOf("\\") >= 0) {
      filename = filename.replaceAll("\\", "/");
    }
    $X.ajax("http://localhost", {
      method: "POST",
      header: { token: "_^WUSV" },
      data: { oprt: "get", file: filename },
      success: callback,
    });
  };
  /**
   * 向本地服务器上传文件
   * @param {String} path
   * @param {(String|Blob)} dat
   * @param {Function} callback function(xhr.response)
   */
  $X.PostFile = (path, dat, callback) => {
    // 先发不带数据的预检请求，否则上传完了结果无权创建文件就emo了，也可以认为是先创建文件，服务器不会检查是否存在，直接覆盖，有需要可js检查Folder.file
    let pxhr = new XMLHttpRequest();
    path = "http://localhost/" + path;
    pxhr.open("POST", path);
    pxhr.setRequestHeader("token", "_^WUSV");
    pxhr.setRequestHeader("Content-Type", "application/octet-stream");
    pxhr.onload = () => {
      if (pxhr.status === 200) {
        let xhr = new XMLHttpRequest();
        xhr.open("POST", path), xhr.setRequestHeader("token", "_^WUSV");
        xhr.setRequestHeader("Content-Type", "application/octet-stream"),
          (xhr.onload = () => {
            if (xhr.status === 200) {
              console.log("Post file successfully", path);
              callback && callback(xhr.response);
            } else {
              console.log("Failed to post file", path);
            }
          }),
          (xhr.onerror = () => {
            console.log("Failed to post file", path, xhr.responseText);
          }),
          xhr.send(dat);
      }
    };
    pxhr.onerror = () => {
      console.log("Error!", pxhr.response);
    };
    pxhr.send();
  };
  /**
   * Image 格式转换器
   */
  class ImageConverter {
    constructor() {
      this.canvas = document.createElement("canvas");
      this.context = this.canvas.getContext("2d");
    }
    /**
     *
     * @param {String} src image url
     * @returns Promise<Image>
     */
    async loadImage(src) {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.crossOrigin = "Anonymous";
        image.onload = () => resolve(image);
        image.onerror = reject;
        image.src = src;
      });
    }

    svgToBase64(svgText) {
      return `data:image/svg+xml;base64,${btoa(svgText)}`;
    }

    async base64ToPng(base64, name) {
      const image = await this.loadImage(base64);
      this.canvas.width = image.width;
      this.canvas.height = image.height;
      this.context.drawImage(image, 0, 0);
      this.downloadImage(this.canvas.toDataURL("image/png"), name);
    }

    async svgToPng(svgText, name) {
      const base64Svg = this.svgToBase64(svgText);
      await this.base64ToPng(base64Svg, name);
    }

    async pngToBase64(src) {
      const image = await this.loadImage(src);
      this.canvas.width = image.width;
      this.canvas.height = image.height;
      this.context.drawImage(image, 0, 0);
      return this.canvas.toDataURL("image/png");
    }

    downloadImage(dataUrl, name) {
      const link = document.createElement("a");
      link.href = dataUrl;
      link.download = name;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    }
    /**
     * 从img src url复制图片
     * Firefox>=127, Chrome>=98 (ClipboardItem)
     * @param {String} src
     */
    async copyImage(src) {
      const image = await this.loadImage(src);
      this.canvas.width = image.width;
      this.canvas.height = image.height;
      this.context.drawImage(image, 0, 0);
      this.canvas.toBlob(async (blob) => {
        try {
          const item = new ClipboardItem({ "image/png": blob });
          await navigator.clipboard.write([item]);
        } catch (error) {
          console.error("Error copying image to clipboard:", error);
        }
      });
    }
  }
  $X.IMG = new ImageConverter();

  class MWJobs {
    // 返回结果为
    static firmURL = {
      Ferring: { url: "https://ferring.wd3.myworkdayjobs.com/wday/cxs/ferring/Ferring", groupName: "jobFamilyGroup" },
      Vertex: { url: "https://vrtx.wd5.myworkdayjobs.com/wday/cxs/vrtx/vertex_careers", groupName: "jobFamilyGroup" },
      Sarepta: { url: "https://sarepta.wd5.myworkdayjobs.com/wday/cxs/sarepta/sarepta_external", groupName: "workerSubType" },
      CSL: { url: "https://csl.wd1.myworkdayjobs.com/wday/cxs/csl/CSL_External", groupName: "jobFamily" },
      Amgen: { url: "https://amgen.wd1.myworkdayjobs.com/wday/cxs/amgen/Careers", groupName: "jobFamilyGroup" },
    };
    firm = "";
    url = "";
    total = 0;
    sum = 0;
    num = 0;
    jobs = {};
    jobTypes = [];
    groupName = "";
    token = "";
    connectLimit = 20;
    header = {
      jobList: {
        "X-Calypso-Csrf-Token": this.token,
        Accept: "application/json",
        "Accept-Language": "en-US",
      },
      jobDetail: {
        Accept: "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept-Language": "en-US",
      },
    };
    // 1. 获得基本信息
    constructor(firm, tok) {
      if (!MWJobs.firmURL[firm] || !tok) {
        console.error("MWJobs construct need 2 parameter!\n1. firm: company name, in MWJobs.firmURL;\n2. tok: X-Calypso-Csrf-Token");
        return null;
      }
      tok && (this.token = tok);
      this.url = MWJobs.firmURL[firm].url;
      this.groupName = MWJobs.firmURL[firm].groupName;
      this.firm = firm;
      this.header.jobList["X-Calypso-Csrf-Token"] = this.token;
    }

    // 获得所有职位基本信息
    async aGetAllJobs() {
      console.log("Getting jobTypes info... from", this.url);
      this.date = new Date().get;
      // 获取基础信息
      const response = await $X.ajaxPro("POST", this.url + "/jobs", { appliedFacets: {}, limit: 20, offset: 0, searchText: "" }, this.header.jobList);
      let res = JSON.parse(response);
      this.jobTypes = res.facets || [];
      this.total = res.total;
      for (const i in this.jobTypes) {
        if (this.jobTypes[i].facetParameter === this.groupName) {
          this.jobTypes = this.jobTypes[i].values;
          break;
        }
      }
      console.log("Total:", this.total, "Job Types:", this.jobTypes.length);
      this.jobs = {};
      // 获取每个jobType的所有工作基础信息
      // forEach全部并发执行，且不等待回调函数执行完毕；for循环一步一步完成
      // 需要顺序的不要用forEach，因为每个循环几乎同时执行;for循环则有顺序
      let reqNum = 0;
      let jobTypeLen = this.jobTypes.length;
      for (let i = 0; i < jobTypeLen; ++i) {
        let offset = 0,
          lmt = 20,
          d = 0;
        let e = this.jobTypes[i];
        e.count < 30 && e.count >= 20 && (lmt = 15);
        while (offset < e.count) {
          // 这个offset有问题，写着total有21个，实际只有20个，然后以20为offset获取下一页时，没有数据于是又返回全部20个数据，c
          // 后来发现可能是 服务器 的数据库访问可能有问题，因为按limit:15,offset:0获取第一页后，第二页offset:15能正常获取到6个
          // 在JSON中，方括号 [key] 允许我们使用变量 key 的值作为对象的键
          $X.ajaxPro("POST", this.url + "/jobs", { appliedFacets: { [this.groupName]: [e.id] }, limit: lmt, offset: offset, searchText: "" }, this.header.jobList, 5000)
            .then((res) => {
              --reqNum;
              if (!this.jobs[e.descriptor]) this.jobs[e.descriptor] = [];
              this.jobs[e.descriptor] = this.jobs[e.descriptor].concat(JSON.parse(res).jobPostings);
            })
            .catch((r) => {
              --reqNum;
              console.log("Failed:", e.descriptor, offset, "\nResponse:", r);
            });
          // 连接数大于限制，等待几秒
          ++reqNum;
          while (reqNum >= this.connectLimit) {
            await new Promise((resolve) => setTimeout(resolve, 2000));
          }
          // 针对还差20来个的特别调整，防止虚标又来重复数据
          offset += lmt;
          d = e.count - offset;
          if (d < 30 && d > 20) {
            lmt = 15;
          }
        }
      }
      this.sum = 0;
      for (let i = 0; i < this.jobTypes.length; ++i) {
        this.sum += this.jobTypes[i].count || this.jobTypes[i].values.length;
      }
      console.log("Basic info loaded. Claimed total:", this.total, " Actual total:", this.sum);
      console.log("Now you can save json by calling cToJSON() before bGetDetail().");
    }
    // 获得所有职位详细信息
    async bGetDetail() {
      // 这里还是用并发好，太多了，一个一个来（500ms一个）太慢了
      // 由于使用全并发，服务器可能无法及时全部响应，因此timeout需要设置得大一些
      this.num = 0;
      console.log("Start getting datailed information...");
      let l = 0,
        reqNum = 0;
      for (const k in this.jobs) {
        l = this.jobs[k].length;
        this.num += l;
        for (let i = 0; i < l; i++) {
          let e = this.jobs[k][i];
          if (!e.externalPath) {
            console.log(k, e, "No externalPath!");
            this.jobs[k].pop;
            continue;
          }
          $X.ajaxPro("GET", this.url + e.externalPath, this.header.jobDetail, 0, 10000)
            .then((r) => {
              --reqNum;
              e.detail = JSON.parse(r).jobPostingInfo;
            })
            .catch((r) => {
              --reqNum;
              console.log("Fail:", k, e, "\nReason:", r);
            });
          ++reqNum;
          while (reqNum >= this.connectLimit) {
            await new Promise((resolve) => setTimeout(resolve, 2000));
          }
        }
        console.log("Done:", k);
      }
      console.log("Loaded Job number", this.num);
    }
    // 输出到HTML
    cToHTML(name = this.firm + "_jobs.html") {
      let str =
        '<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>' +
        this.firm +
        '</title><style>p.h1,p.h2,p.h3{font-weight:bold;}p.h1{font-size:18px;}p.h2{font-size:16px;}p.h3{font-size:16px;}body{counter-reset:h2;}.card>h2{counter-increment:h2}.card>h2::before{content:counter(h2) " "}.desc h2{font-size:16px;font-weight:400}</style></head><body>\n';
      for (const k in this.jobs) {
        str += "<h1>Type: " + k + '</h1>\n<div class="en">\n';
        for (let i = 0; i < this.jobs[k].length; ++i) {
          let e = this.jobs[k][i].detail;
          if (!e) {
            continue;
          }
          str += '<div class="card">\n<h2>' + e.title + "</h2>\n";
          str += '<p class="prop"><b>Location</b>: ' + e.location + "</p>\n";
          str += '<p class="prop"><b>TimeType</b>: ' + e.timeType + "</p>\n";
          str += '<p class="prop"><b>PostDate</b>: ' + e.startDate + "</p>\n";
          str += '<p class="prop"><b>JobID</b>: ' + e.jobReqId + "</p>\n";
          str += '<p class="prop"><b>Description</b></p>\n<div class="desc">' + e.jobDescription + "</div>\n</div>\n"; // 结束 div.desc和.card 标签
        }
        str += "</div>\n"; // 结束 div.en 标签
      }
      str += '\n</body><script>window.onload=()=>{ document.designMode="on";}</script></html>';
      // 全局替换
      str = str.replace(/‘|’/g, "'").replace(/“|”/g, '"').replace(/ /g, " ");
      $X.saveFile(str, name);
    }
    cToJSON(name = this.firm + "_jobs.json") {
      let str = JSON.stringify(this.jobs);
      str = str.replace(/‘|’/g, "'").replace(/“|”/g, '\\"').replace(" ", " ");
      $X.saveFile(str, name);
    }
    cToAbstract(name = this.firm + "_readme.txt") {
      let str = "公司名称：" + this.firm + "\n招聘网页：" + location.href + "\nAPI地址 ：" + this.url + "\n获取时间：" + $X.strfTime("%Y/%m/%d %H:%M:%S\n\n");
      str += `名义总数：${this.total}，分类总数：${this.sum}，获得总数：${this.num}\n`;
      str += "职务类别：" + this.jobTypes.length + "\n";
      for (const k in this.jobs) {
        str += k + "\t" + this.jobs[k].length + "\n";
      }
      $X.saveFile(str, name);
    }
    cToCMD() {
      // 用来创建 docx 空文档的命令
      let c = 'type nul>"',
        d = '.docx"\n',
        zd = '_zh.docx"\n';
      let str = "";
      for (let i = 0; i < this.jobTypes.length; ++i) {
        str += c + this.jobTypes[i].descriptor + d;
        str += c + this.jobTypes[i].descriptor + zd;
      }
      console.log(str);
      return str;
    }
    getJob(externalPath) {
      $X.ajaxPro("GET", this.url_base + externalPath, "", this.header.jobDetail)
        .then((v) => console.log(v))
        .catch((e) => console.log(e));
    }
  }
  unsafeWindow.MWJobs = MWJobs;
  unsafeWindow.MixEnZh = () => {
    // 结构 <div#en><div.card> <div#zh><div.card> 有多个Type也行
    let en = $X.all("div.en>div.card");
    let zh = $X.all("div.zh>div.card");
    if (en.length !== zh.length) {
      console.log("zh en 长度不等");
      return;
    }
    let len = en.length;
    // 由于改变结构，nodelist好像也会变，故应该从后往前；最后一个没有nextElement直接appendChild就行
    let next = null;
    while (--len >= 0) {
      next = en[len].nextElementSibling;
      next ? en[len].parentElement.insertBefore(zh[len], next) : en[len].parentElement.appendChild(zh[len]);
    }
  };

  class JoVE {
    constructor() {
      this.url = location.pathname;
      this.res = null;
      this.tmout = 8000;
      this.fail = 10;
      this.series = "";
      document.addEventListener("keydown", this.start);
    }
    async start(e) {
      if (e && !(e.ctrlKey && e.key === "Enter")) {
        return;
      }
      if ($X(".landing-section-list")) {
        console.log("JoVE - edu_xxx All Courses in Certain Subject");
        this.res = this.edu_xxx();
      } else if ($X(".catalog-list")) {
        this.series = "sci";
        console.log("JoVE - sci_edu  All sub-classes of a Sci-Edu course");
        this.res = await this.sci_edu();
      } else if ($X(".chapter-body-content")) {
        this.series = "core";
        console.log("JoVE - chapter  All lessons of a Core chapter");
        this.res = await this.chapter();
      } else if (this.url.match(/^\/education\/\d+/)) {
        this.series = "sci";
        console.log("JoVE - edu_video  All lessons of a Sci-Edu sub-class");
        this.res = await this.edu_video();
      } else if (this.url.match(/science-education-library\/\d+\/.*/)) {
        this.series = "lab";
        console.log("JoVE - lab_manual  All lessons of a Lab Manual course");
        this.res = await this.lab_manual();
      } else if (this.url.match(/science-education\/.*/)) {
        this.series = "core";
        console.log("JoVE - Core  All chapters of a Core course");
        this.res = await this.core();
      }
      console.log("Done");
      console.log(this.res);
      navigator.clipboard.writeText(JSON.stringify(this.res));
    }
    saveDoc() {
      let str = '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Title</title><style>.subscription_required{display:none;}</style></head><body>\n';
      if (this.series === "core") {
        // collection 层
        for (let i = 0; i < this.res.length; i++) {
          const ch = this.res[i].chapter;
          str += `<div>\n<h1>${this.res[i].name}</h1>\n`;
          // chapters 层
          for (let j = 0; j < ch.length; j++) {
            const c = ch[j];
            str += `<h2>${c.name}</h2>\n`;
            // concept、practice层
            for (let k = 0; k < c.concept.length; k++) {
              const v = c.concept[k];
              str += `<h3>${v.name}</h3>\n<div class="content">${v.content}</div>\n`;
            }
            for (let k = 0; k < c.practice.length; k++) {
              const v = c.practice[k];
              str += `<h3>${v.name}</h3>\n<div class="content">${v.content}</div>\n`;
            }
          }
          str += "</div>\n";
        }
      } else if (this.series === "lab") {
        for (let i = 0; i < this.res.length; i++) {
          const items = this.res[i].item;
          str += `<div>\n<h2>${this.res[i].name}</h2>\n`;
          for (let j = 0; j < items.length; j++) {
            const v = items[j];
            str += `<h3>${v.name}</h3>\n<div class="content">${v.content}</div>\n`;
          }
          str += "</div>";
        }
      } else if (this.series === "sci") {
        for (let i = 0; i < this.res.length; i++) {
          const items = this.res[i].video;
          str += `<div>\n<h2>${this.res[i].name}</h2>\n`;
          for (let j = 0; j < items.length; j++) {
            const v = items[j];
            str += `<h3>${v.name}</h3>\n<div class="content">${v.content}</div>\n`;
          }
          str += "</div>";
        }
      }
      str += "</body></html>";
      $X.saveFile(str, "export.html");
    }
    tocmd(id, name, dir="") {
      dir&& dir.charAt(0)!=='\\' && (dir='\\'+dir);
      return 'N_m3u8DL-CLI_v3.0.2 "https://jovecdn2.ccindex.cn/CDNSource/hls/' + `${id}/${id}` + '_.m3u8" --workDir "D:\\Download\\Video'+dir+'" --saveName "' + name + '" --maxThreads 24 --minThreads 12 --retryCount 20 --timeOut 3 --enableDelAfterDone --enableMuxFastStart --disableIntegrityCheck';
    }
    mkdir(list) {
      if (list instanceof Array) {
        let str = "";
        for (let i = 0; i < list.length; i++) {
          const e = list[i];
          str += `mkdir "${i + 1}. ${e.name}"\n`;
        }
        return str;
      }
    }
    getVideo(list, dir="") {
      if (list instanceof Array) {
        let i = 0;
        $X.runcmd(this.tocmd(list[i].id, `${i+1}. `+list[i].name.replace(":", " "),dir));
        let itv = setInterval(() => {
          if (++i >= list.length) {
            clearInterval(itv);
            return;
          }
          $X.runcmd(this.tocmd(list[i].id, list[i].name.replace(":", " "),dir));
        }, 10000);
      }
    }
    // All Courses in Certain Subject
    edu_xxx() {
      let res = [];
      let card = $X.all(".landing-section-card>a");
      for (let i = 0; i < card.length; ++i) {
        const v = card[i];
        let data = { list: [] };
        data.series = $X(".subject-prod span", v).innerText;
        data.info = $X(".subject-desc", v).innerText;
        data.name = $X("h5", v).innerText.replace(/\s+/g, " ").trim();
        data.href = v.href;
        data.cover = $X(".landing-card-wrapper", v).style.backgroundImage.slice(5, -2);
        res.push(data);
      }
      return res;
    }
    // All chapters of a Core course
    async core() {
      let res = [];
      let col = $X.all(".collection-name");
      if (col.length === 0) {
        col = [document];
      }
      for (let i = 0; i < col.length; ++i) {
        const v = col[i];
        let data = { chapter: [] };
        data.name = (($X("h2", v) || {}).innerText || "Default").replace(/\s+/g, " ").trim();
        let ch = $X.all(".chapter-card", v.nextElementSibling);
        console.log(data.name, `${i + 1}/${col.length}`);
        for (let j = 0; j < ch.length; ++j) {
          const c = ch[j];
          let d = {};
          d.cover = $X("img", c).src;
          let link = $X(".chapter-link", c);
          d.name = link.innerText.replace(/\s+/g, " ").trim().replace("\n", " ");
          d.href = link.href;
          let cur = j + 1;
          let fail = this.fail;
          while (fail)
            await $X
              .ajaxPro("GET", d.href, null, null, this.tmout)
              .then(async (v) => {
                let s = v.indexOf("<main");
                let e = v.indexOf("</main") + 7;
                let div = $X.createEle("div");
                div.innerHTML = v.slice(s, e);
                let cp = await this.chapter(div);
                d.concept = cp.concept;
                d.practice = cp.practice;
                div.remove();
                fail = 0;
              })
              .catch((v) => {
                console.log("Fail on chapter html of Core course", d.href, v);
                fail--;
              });
          console.log(d.name, `${cur}/${ch.length}`);
          data.chapter.push(d);
        }
        res.push(data);
      }
      return res;
    }
    // All lessons of a Lab Manual course
    async lab_manual() {
      let res = [];
      let col = $X.all(".collection-name");
      let p = $X.createEle("p");
      let fail = this.fail;
      if (col.length === 0) {
        col = [document];
      }
      for (let i = 0; i < col.length; ++i) {
        const v = col[i];
        let data = { item: [] };
        data.name = (($X("h2", v) || {}).innerText || "Default").replace(/\s+/g, " ").trim();
        let ch = $X.all(".lm-item-card", v.nextElementSibling || v);
        console.log(data.name, `${i + 1}/${col.length}`);
        for (let j = 0; j < ch.length; ++j) {
          const c = ch[j];
          let d = {};
          d.cover = $X("img", c).src;
          let link = $X("h3 .lm-item-link", c);
          d.name = link.innerText.replace(/\s+/g, " ").trim();
          d.href = link.href;
          d.id = d.href.match(/\d+/)[0];
          console.log(d.name, `${j + 1}/${ch.length}`);
          fail = this.fail;
          while (fail) {
            await $X
              .ajaxPro("GET", d.href, null, null, this.tmout)
              .then((v) => {
                let s = v.indexOf('<div id="transcript');
                let e = v.indexOf("</div", s) + 6;
                p.innerHTML = v.slice(s, e).replace(/\t+/g, "\t");
                d.transcript = p.innerHTML;
                s = v.indexOf('<section class="lab-section', e);
                e = v.indexOf("</section", s) + 10;
                p.innerHTML = v.slice(s, e);
                let h3 = $X("h3", p);
                h3 && h3.remove();
                d.content = p.innerHTML.replace(/\t+/g, "\t");
                fail = 0;
              })
              .catch((v) => {
                console.log("Failed:", d.href, v);
                fail--;
              });
          }
          data.item.push(d);
        }
        res.push(data);
      }
      p.remove();
      return res;
    }
    // All sub-classes of a Sci-Edu course
    async sci_edu() {
      let res = [];
      let items = $X.all(".catalog-category-item");
      for (let i = 0; i < items.length; i++) {
        let dat = {};
        const item = items[i];
        dat.name = $X("h3", item).innerText.replace(/\s+/g, " ").trim();
        dat.cover = $X("a", item).style.backgroundImage.slice(5, -2);
        dat.href = $X("a", item).href;
        dat.info = $X("span", item).innerText.replace(/\s+/g, " ").trim();
        let cur = i + 1;
        let fail = this.fail;
        while (fail)
          await $X
            .ajaxPro("GET", dat.href, null, null, this.tmout)
            .then(async (v) => {
              let s = v.indexOf("<main");
              let e = v.indexOf("</main") + 7;
              let div = $X.createEle("div");
              div.innerHTML = v.slice(s, e);
              let vd = await this.edu_video(div);
              dat.video = vd;
              div.remove();
              fail = 0;
            })
            .catch((v) => {
              console.log("Fail on sub-class html of Sci-edu course", dat.href, v);
              fail--;
            });
        console.log(dat.name, `${cur}/${items.length}`);
        res.push(dat);
      }
      return res;
    }
    // All lessons of a Sci-Edu sub-class
    async edu_video(ele) {
      let res = [];
      let vids = $X.all(".catalog-video-card", ele);
      let fail = this.fail;
      for (let i = 0; i < vids.length; i++) {
        const vid = vids[i];
        let dat = {};
        let h2a = $X("h2 a", vid);
        dat.name = h2a.innerText.replace(/\s+/g, " ").trim();
        dat.href = h2a.href;
        dat.cover = $X("img", vid).src;
        dat.content = "";
        dat.id = dat.href.match(/\d+/)[0];
        console.log(dat.name, `${i + 1}/${vids.length}`);
        fail = this.fail;
        while (fail)
          await $X
            .ajaxPro("GET", dat.href, null, null, this.tmout)
            .then((v) => {
              let s = v.indexOf('<article class="no-access');
              let e = v.indexOf("</article", s) + 10;
              dat.content = v.slice(s, e).replace(/\t+/g, "\t");
              fail = 0;
            })
            .catch((v) => {
              console.log("Failed:", dat.href, v);
              fail--;
            });
        res.push(dat);
      }
      return res;
    }
    // All lessons of a Core chapter
    async chapter(ele = null) {
      let res = {
        concept: [],
        practice: [],
      };
      let concept = $X.all("#keyterms_tab .lesson", ele);
      let practice = $X.all("#scientists_tab .lesson", ele);
      let p = $X.createEle("p");
      let fail = this.fail;
      for (let i = 0; i < concept.length; i++) {
        const les = concept[i];
        let dat = {};
        dat.href = $X("a", les).href;
        dat.cover = $X("img", les).src;
        dat.id = dat.href.match(/\d+/)[0];
        dat.name = $X("h2", les).innerText.replace(/\s+/g, " ").trim();
        dat.info = $X(".lesson-desc", les).innerText.replace(/\s+/g, " ").trim();
        console.log(dat.name, `${i + 1}/${concept.length}`);
        fail = this.fail;
        while (fail)
          await $X
            .ajaxPro("GET", dat.href, null, null, this.tmout)
            .then((v) => {
              let s = v.indexOf('<div id="transcript');
              let e = v.indexOf("</div", s) + 6;
              p.innerHTML = v.slice(s, e);
              dat.transcript = p.innerHTML.replace(/\t+/g, "\t");
              s = v.indexOf('<div class="lesson-text', e);
              e = v.indexOf('<div class="section-sep', s);
              p.innerHTML = v.slice(s, e) + "</div>";
              let h3 = $X("h3", p);
              h3 && h3.remove();
              dat.content = p.innerHTML.replace(/\t+/g, "\t");
              fail = 0;
            })
            .catch((v) => {
              console.log("Failed:", dat.href, v);
              fail--;
            });
        res.concept.push(dat);
      }
      for (let i = 0; i < practice.length; i++) {
        const les = practice[i];
        let dat = {};
        dat.href = $X("a", les).href;
        dat.id = dat.href.match(/\d+/)[0];
        dat.cover = $X("img", les).src;
        dat.name = $X("h2", les).innerText.replace(/\s+/g, " ").trim();
        dat.info = $X(".lesson-desc", les).innerText.replace(/\s+/g, " ").trim();
        console.log(dat.name, `${i + 1}/${practice.length}`);
        fail = this.fail;
        while (fail)
          await $X
            .ajaxPro("GET", dat.href, null, null, this.tmout)
            .then((v) => {
              let s = v.indexOf('<div class="video-text"');
              let e = v.indexOf("<h3>Tags", s);
              p.innerHTML = v.slice(s, e) + "</div></div>";
              let h1 = $X("h1", p);
              h1 && h1.remove();
              (h1 = $X('iframe',p)) && h1.remove();
              (h1=$X('h3.jove-flex',p)) && h1.remove();
              (h1=$X('h3.research-chapters',p)) && h1.remove();
              dat.content = p.innerHTML;
              fail = 0;
            })
            .catch((v) => {
              console.log("Failed:", dat.href, v);
              fail--;
            });
        res.practice.push(dat);
      }
      p.remove();
      return res;
    }
  }
  if (location.href.match(/jove\.com/)) {
    $X.JoVE = new JoVE();
  }
  console.log("Base Package loaded. Version:", $X.version);
})();
function tocmd(id, name, dir="") {
  dir&& dir.charAt(0)!=='\\' && (dir='\\'+dir);
  return 'N_m3u8DL-CLI_v3.0.2 "https://jovecdn2.ccindex.cn/CDNSource/hls/' + `${id}/${id}` + '_.m3u8" --workDir "D:\\Download\\Video'+dir+'" --saveName "' + name + '" --maxThreads 24 --minThreads 12 --retryCount 20 --timeOut 3 --enableDelAfterDone --enableMuxFastStart --disableIntegrityCheck';
}
function getVideo(list, dir="",time=10000) {
  if (list instanceof Array) {
    let i = 0;
    $X.runcmd(tocmd(list[i].id, `${i+1}. `+list[i].name.replace(":", " "),dir));
    let itv = setInterval(() => {
      if (++i >= list.length) {
        clearInterval(itv);
        return;
      }
      $X.runcmd(tocmd(list[i].id, `${i+1}. `+list[i].name.replace(":", " "),dir));
    }, time);
  }
  document.addEventListener()
}
